package org.adorsys.aderp.aderplogin.client.hessian;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.adorsys.aderp.aderplogin.client.common.Oauth2User;
import org.adorsys.aderp.aderplogin.client.common.Oauth2UserProvider;
import org.adorsys.aderp.aderplogin.client.oauth.Oauth2SystemUserService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.auth.ClientAuthenticationHandler;
import org.springframework.security.oauth2.client.token.auth.DefaultClientAuthenticationHandler;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.http.converter.FormOAuth2ExceptionHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpMessageConverterExtractor;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

public class Oauth2UserProviderForClient implements InitializingBean, Oauth2UserProvider {

	private static final FormHttpMessageConverter FORM_MESSAGE_CONVERTER = new FormHttpMessageConverter();
	private final RestTemplate restTemplate;

	private List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
	private ClientAuthenticationHandler authenticationHandler = new DefaultClientAuthenticationHandler();
	
	// set at initialization
	private OAuth2ProtectedResourceDetails loginResource;
	
	private Oauth2SystemUserService oauth2SystemUserService;

	public Oauth2UserProviderForClient() {
		this.restTemplate = new RestTemplate();
		this.restTemplate.setErrorHandler(new UserEndpointErrorHandler());
		this.restTemplate.setRequestFactory(new SimpleClientHttpRequestFactory() {
			@Override
			protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
				super.prepareConnection(connection, httpMethod);
				connection.setInstanceFollowRedirects(false);
			}
		});
		setMessageConverters(this.restTemplate.getMessageConverters());
	}

	protected RestTemplate getRestTemplate() {
		return restTemplate;
	}
    
    public Oauth2User getUser(String token){
    	// catch and return built in system user
		Oauth2User systemUser = oauth2SystemUserService.getSystemUser(token);
		if(systemUser!=null) return systemUser;

		try {
			// Prepare headers and form before going into rest template call in case the URI is affected by the result
			HttpHeaders headers = new HttpHeaders();
			MultiValueMap<String, String> form = getParametersForUserRequest(token);
			authenticationHandler.authenticateTokenRequest(loginResource, form , headers);

			return getRestTemplate().execute(getUserEndpointUri(loginResource, form), getHttpMethod(),
					getRequestCallback(loginResource, form, headers), getResponseExtractor(), form.toSingleValueMap());

		}
		catch (OAuth2Exception oe) {
			throw new OAuth2AccessDeniedException("Can not obtain oauth user.", loginResource, oe);
		}
		catch (RestClientException rce) {
			throw new OAuth2AccessDeniedException("Error requesting oauth user.", loginResource, rce);
		}
    }
    
	private MultiValueMap<String, String> getParametersForUserRequest(String token) {
		
		MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
		form.add("token", token);
		return form;
	}
	
	protected String getUserEndpointUri(OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form) {

		String accessTokenUri2 = resource.getAccessTokenUri();
		String userEndpointUri= accessTokenUri2.replace("token", "user");

		StringBuilder builder = new StringBuilder(userEndpointUri);
		return builder.toString();
	}
	
    
	protected HttpMethod getHttpMethod() {
		return HttpMethod.POST;
	}

	/**
	 * Request callback implementation that writes the given object to the request stream.
	 */
	private class OAuth2UserCallback implements RequestCallback {

		private final MultiValueMap<String, String> form;

		private final HttpHeaders headers;

		private OAuth2UserCallback(MultiValueMap<String, String> form, HttpHeaders headers) {
			this.form = form;
			this.headers = headers;
		}

		public void doWithRequest(ClientHttpRequest request) throws IOException {
			request.getHeaders().putAll(this.headers);
			request.getHeaders().setAccept(
					Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON));
			FORM_MESSAGE_CONVERTER.write(this.form, MediaType.APPLICATION_FORM_URLENCODED, request);
		}
	}

	private class UserEndpointErrorHandler extends DefaultResponseErrorHandler {

		@SuppressWarnings("unchecked")
		@Override
		public void handleError(ClientHttpResponse response) throws IOException {
			for (HttpMessageConverter<?> converter : messageConverters) {
				if (converter.canRead(OAuth2Exception.class, response.getHeaders().getContentType())) {
					throw ((HttpMessageConverter<OAuth2Exception>)converter).read(OAuth2Exception.class, response);
				}
			}
			super.handleError(response);
		}

	}


	protected ResponseExtractor<Oauth2User> getResponseExtractor() {
		return new HttpMessageConverterExtractor<Oauth2User>(Oauth2User.class, this.messageConverters);
	}

	protected RequestCallback getRequestCallback(OAuth2ProtectedResourceDetails resource,
			MultiValueMap<String, String> form, HttpHeaders headers) {
		return new OAuth2UserCallback(form, headers);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		Assert.notNull(loginResource, "Missing dependency with name loginResource and type OAuth2ProtectedResourceDetails");
		Assert.notNull(oauth2SystemUserService, "Missing dependency with name oauth2SystemUserService and type Oauth2SystemUserService");
	}
	
	public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
		this.messageConverters = new ArrayList<HttpMessageConverter<?>>(messageConverters);
//		this.messageConverters.add(new Oauth2UserHttpJsonMessageConverter());
		this.messageConverters.add(new Oauth2UserHttpFormMessageConverter());
		this.messageConverters.add(new FormOAuth2ExceptionHttpMessageConverter());
	}

	public void setAuthenticationHandler(
			ClientAuthenticationHandler authenticationHandler) {
		this.authenticationHandler = authenticationHandler;
	}

	public void setLoginResource(OAuth2ProtectedResourceDetails loginResource) {
		this.loginResource = loginResource;
	}

	public void setOauth2SystemUserService(
			Oauth2SystemUserService oauth2SystemUserService) {
		this.oauth2SystemUserService = oauth2SystemUserService;
	}
	
	
}
